/*
 * @(#)TelnetTool.java        1.0 07/11/13
 *
 * Copyright 2007 Linkage, Inc.
 */
package com.linkage.olcom.common.telnet;

import org.apache.commons.net.telnet.EchoOptionHandler;
import org.apache.commons.net.telnet.InvalidTelnetOptionException;
import org.apache.commons.net.telnet.SuppressGAOptionHandler;
import org.apache.commons.net.telnet.TelnetClient;
import org.apache.commons.net.telnet.TelnetNotificationHandler;
import org.apache.commons.net.telnet.TerminalTypeOptionHandler;
import org.apache.log4j.Logger;

import com.linkage.olcom.common.StringUtil;

import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;

import java.net.SocketException;
import java.net.SocketTimeoutException;

/**
 * 启动一个TELENT客户端线程、它响应向主机上的TELNET服务发送指令并执行、同时获取指令执行的结果（具有线程安全性）
 * <p>
 * Telnet的工作原理 本地机上的客户程序完成如下功能：
 * <ul>
 * <li> 1) 建立与服务器的TCP联接。
 * <li> 2) 从键盘上接收你输入的字符。
 * <li> 3) 把你输入的字符串变成标准格式并送给远程服务器。
 * <li> 4) 从远程服务器接收输出的信息。
 * <li> 5) 把该信息显示在你的屏幕上。
 * </ul>
 * <p>
 * 远程计算机的“服务”程序完成如下功能:
 * <ul>
 * <li> 1) 通知你的计算机，远程计算机已经准备好了。
 * <li> 2) 等候你输入命令。
 * <li> 3) 对你的命令作出反应（如显示目录内容，或执行某个程序等）。
 * <li> 4) 把执行命令的结果送回给你的计算机。
 * <li> 5) 重新等候你的命令。
 * </ul>
 * <p>
 * Telnet远程登录服务分为以下4个过程：
 * <ul>
 * <li> 1）本地与远程主机建立连接。该过程实际上是建立一个TCP连接，用户必须知道远程主机的Ip地址或域名；
 * <li> 2）将本地终端上输入的用户名和口令及以后输入的任何命令或字符以NVT（Net
 * VirtualTerminal）格式传送到远程主机。该过程实际上是从本地主机向远程主机发送一个IP数据报；
 * <li> 3）将远程主机输出的NVT格式的数据转化为本地所接受的格式送回本地终端，包括输入命令回显和命令执行结果；
 * <li> 4）最后，本地终端对远程主机进行撤消连接。该过程是撤销一个TCP连接。
 * </ul>
 * <p>
 * 它提供了三种基本服务：
 * <ul>
 * <li> 1）Telnet定义一个网络虚拟终端为远的系统提供一个标准接口。客户机程序不必详细了解远的系统，他们只需构造使用标准接口的程序；
 * <li> 2）Telnet包括一个允许客户机和服务器协商选项的机制，而且它还提供一组标准选项；
 * <li> 3）Telnet对称处理连接的两端，即Telnet不强迫客户机从键盘输入，也不强迫客户机在屏幕上显示输出。
 * </ul>
 * <p>
 * 适应异构：
 * <ul>
 * <li>
 * 对于发送的数据：客户机软件把来自用户终端的按键和命令序列转换为NVT格式，并发送到服务器，服务器软件将收到的数据和命令，从NVT格式转换为远地系统需要的格式；
 * <li> 对于返回的数据：远地服务器将数据从远地机器的格式转换为NVT格式，而本地客户机将将接收到的NVT格式数据再转换为本地的格式。
 * </ul>
 * <p>
 * 传送远地命令：
 * <ul>
 * <li> Telnet同样使用NVT来定义如何从客户机将控制功能传送到服务器。我们知道USASCII字符集包括95个可打印字符和33个控制码。
 * <li>
 * 当用户从本地键入普通字符时，NVT将按照其原始含义传送；当用户键入快捷键（组合键）时，NVT将把它转化为特殊的ASCII字符在网络上传送，并在其到达远地机器后转化为相应的控制命令。
 * </ul>
 * <p>
 * 将正常ASCII字符集与控制命令区分主要有两个原因：
 * <ul>
 * <li> 1）这种区分意味着Telnet具有更大的灵活性：它可在客户机与服务器间传送所有可能的ASCII字符以及所有控制功能；
 * <li> 2）这种区分使得客户机可以无二义性的指定信令，而不会产生控制功能与普通字符的混乱。
 * </ul>
 * <p>
 * 选项协商：
 * <ul>
 * <li> 它对于每个选项的处理都是对称的，即任何一端都可以发出协商申请；任何一端都可以接受或拒绝这个申请。
 * <li> 另外，如果一端试图协商另一端不了解的选项，接受请求的一端可简单的拒绝协商。
 * <li> 因此，有可能将更新，更复杂的Telnet客户机服务器版本与较老的，不太复杂的版本进行交互操作。
 * <li> 如果客户机和服务器都理解新的选项，可能会对交互有所改善。否则，它们将一起转到效率较低但可工作的方式下运行
 * </ul>
 * <p>
 * 可以调用 <code>public TelnetTool(TelnetInfo telnetInfo)</code>构造函数直接启动线程,接下来就可以准备工作了
 * <p>
 * <hr>
 * <blockquote>
 * 
 * <pre>
 * 	. . .
 *  // 构造并设置好TelnetInfo
 * 	TelnetTool telnet = new TelnetTool(telnetInfo);
 *  telnet.excute(&quot;who;pwd;ls -la;ps -ef | grep java&quot;);
 * 	. . .
 * </pre>
 * 
 * </blockquote>
 * <hr>
 * <p>
 * 不过你也可以调用默认构造函数并且自己设置<code>TelnetInfo</code>,创建连接,启动线程本身...等一列工作, 其大概代码为：
 * <p>
 * <hr>
 * <blockquote>
 * 
 * <pre>
 * 	. . .
 * 	TelnetTool telnet = new TelnetTool();
 * 	// 构造并设置好TelnetInfo
 * 	telnet.setTelnetInfo(telnetInfo)
 *  telnet.setup(telnetInfo);
 * 	telnet.connect(telnetInfo);
 * 	telnet.start();
 * 	// 为最终执行指令做好准备
 * 	telnet.initCommand(telnetInfo);
 * 	 . . .
 * </pre>
 * 
 * </blockquote>
 * <hr>
 * 
 * @author brucewang
 * @version 1.0 07/11/13
 */
public class TelnetTool implements Runnable, TelnetNotificationHandler {
	public static final String[] ENDFLAG = { "\r\n", "\n" };

	public static final String FINISH_INFO = "command execute finish";

	public static final String NULL_COMMAND = "execute command is null";

	public static final Logger logger = Logger.getLogger(TelnetTool.class
			.getName());
	// 当前执行线程等待结果返回时间
	private final int timeout = 1000 * 5; // 5秒
	// 设定空闲连接保持的时间
	private final int default_connect_timeout = 1000 * 60 * 10;// 10分钟

	private TelnetClient telent = null;

	private String loginPrompt = "$";

	private String prompt = "$";

	private String charSet = "GBK";

	private int capacityFactor = 25;

	private StringBuffer output = new StringBuffer();

	private TelnetInfo telnetInfo;

	private InputStream in;

	private PrintWriter out;

	// 标识指令执行线程是否运行
	private volatile boolean run = false;

	// 标识指令是否执行完毕
	private volatile boolean done = false;

	// 判断用户是否成功登录,如果因为登录错误,则把其设为false;
	private volatile boolean successLogin = true;

	// 指令执行线程
	private Thread executeThread;

	private Object runLock = new Object();

	private Object doneLock = new Object();

	/**
	 * 默认构�1�7�函敄1�7
	 * 
	 */
	public TelnetTool() {

	}

	/**
	 * 在构造函数中启动线程
	 * 
	 * @param telnetInfo
	 */
	public TelnetTool(TelnetInfo telnetInfo) {
		this(telnetInfo, true);
	}

	/**
	 * 
	 * @param telnetInfo
	 * @param start
	 */
	public TelnetTool(TelnetInfo telnetInfo, boolean start) {
		this.telnetInfo = telnetInfo;
		this.setup(telnetInfo);
		this.connect(telnetInfo);
		if (start) {
			this.start();
			logger.info("login:" + successLogin);
			if (this.successLogin) {
				this.initCommand(telnetInfo);// 为最终执行指令做好准备
			}
		}
	}

	/**
	 * 
	 * @param telnetInfo
	 * 
	 * @return boolean
	 */
	public void connect(TelnetInfo telnetInfo) {
		this.connect(telnetInfo.getHost(), telnetInfo.getPort(), telnetInfo
				.getUserName(), telnetInfo.getPassWord(),
				default_connect_timeout);
	}

	/**
	 * 以指定主机和默认的23端口来建立到Telnet服务的连接
	 * 
	 * @param hostip
	 * @param user
	 * @param password
	 * 
	 * @return boolean
	 */
	public void connect(String hostip, String user, String password) {
		this.connect(hostip, 23, user, password, default_connect_timeout);
	}

	/**
	 * 以指定主机和端口来建立到Telnet服务的连接
	 * 
	 * @param hostip
	 * @param port
	 * @param user
	 * @param password
	 * 
	 * @return boolean
	 */
	public void connect(String hostip, int port, String user, String password,
			int timeout) {
		if (hostip == null || user == null || hostip == null) {
			throw new NullPointerException();
		}
		if (port < 0 || port > 65535) {
			throw new IllegalArgumentException("port not in (0,65535)");
		}
		try {
			this.telent = new TelnetClient();
			this.addOptionHandler();
			this.telent.connect(hostip, port);
			// 在与此 Socket 关联的 InputStream 上调用 read() 将只阻塞此时间长度。
			this.telent.setSoTimeout(timeout);
			this.out = new PrintWriter(new BufferedWriter(
					new OutputStreamWriter(telent.getOutputStream())), true);
			this.in = new BufferedInputStream(new DataInputStream(telent
					.getInputStream()));
			// 登录
			read("login: ");
			write(user);
			read("Password: ");
			write(password);
			read(prompt);
			logger.info("user:" + user + " login:" + hostip);
		} catch (SocketTimeoutException ste) {
			logger.error("can't connect to " + hostip
					+ " SocketTimeoutException");
			ste.printStackTrace();
			successLogin = false;
			this.terminate();
		} catch (SocketException se) {
			logger.error("can't connect to " + hostip + " SocketException");
			se.printStackTrace();
			successLogin = false;
			this.terminate();
		} catch (IOException ie) {
			logger.error("can't connect to " + hostip + " IOException");
			ie.printStackTrace();
			successLogin = false;
			this.terminate();
		}
	}

	/**
	 * 
	 * @return
	 */
	public boolean isConnected() {
		if (telent == null)
			return false;
		if (!this.successLogin)
			return false;
		if (!this.isRun())
			return false;
		if (!telent.isConnected())
			return false;
		return true;
	}

	/**
	 * 启动执行线程。
	 * <p>
	 * 如果线程已经启动而因为调用了suspend()方法从而使线程阻塞挂起,那么再次调用start()方法可以恢复线程的运行
	 * <p>
	 * 不管线程是处于何种状态（运行,阻塞...）,多次不断重复的调用start()方法是无碍的
	 * 
	 * <p>
	 * 可以通过调用suspend()方法使执行线程阻塞挂起
	 * 
	 */
	public void start() {
		if (this.executeThread == null) {
			this.run = true; // 设置标志为可运行
			this.executeThread = new Thread(this);
			this.telent.registerNotifHandler(this);
			this.executeThread
					.setPriority(Thread.currentThread().getPriority() - 1);
			this.executeThread.start();
			logger.debug("{ Telnet Service Startup Success }");
		} else {
			synchronized (runLock) {
				this.run = true;
				runLock.notify();
			}
		}
	}

	/**
	 * 向Telnet服务发送指令并执行
	 * 
	 * @return String 返回指令执行结果
	 */
	public String execute(String command) {
		if (!successLogin || !run || out == null)
			return null;
		if (command == null || command.trim().equals(""))
			return NULL_COMMAND;
		final String primeCommand = command;
		this.out.write(this.assembleCommand(command));
		this.out.flush();
		this.logCommand(primeCommand);
		// 等待指令执行完成
		this.done = false;
		int i = 0;
		synchronized (doneLock) {
			while (this.done == false) {
				try {
					// 超时时间 - 如果不设置则在指令无法正常结束返回的情况下会永远阻塞。
					doneLock.wait(timeout);
					if (i++ > 1) {
						this.done = true;
						logger
								.info("lock wait can't notify, cpu nothing loop - terminate");
						// 防止指令无法正常结束返回 导致CPU空转 执行线程永远无法被唤醒
						break;
					}
				} catch (InterruptedException e) {
					logger.error("time out(" + timeout
							+ ") - ExecuteThread interrupt");
					this.done = true;
					this.executeThread.interrupt();
					this.terminate();
					break;
				}
			}
		}
		return this.getResult(); // 指令执行完成,返回执行结果
	}

	/*
	 * Reader thread. Reads lines from the TelnetClient and echoes them on
	 * thescreen.
	 * 
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Runnable#run()
	 */
	public void run() {
		try {
			int size = 1024 * capacityFactor;
			byte[] buff = new byte[size];
			int ret_read = 0;
			do {
				if (this.in == null || this.out == null) {
					break;
				}
				if (this.run && this.in != null) {
					ret_read = this.in.read(buff);
				}
				if (ret_read >= 1) {
					this.appendResult(new String(buff, 0, ret_read));
				}
				String last = StringUtil.getLastLine(this.getOutput()).trim();
				if (last.equals(this.telnetInfo.getEndLine().trim())) {// 判断指令是否执行结束.
					this.done = true;
					synchronized (doneLock) {
						doneLock.notify();
					}
					logger.debug(FINISH_INFO);
				}
				if (Thread.currentThread().isInterrupted()) {
					return;
				}
				synchronized (runLock) {
					while (this.run == false) {
						try {
							runLock.wait();
						} catch (InterruptedException e) {
							this.in.close();
							return;
						}
					}
				}
			} while (ret_read > 0);
			this.done = true;
		} catch (IOException ioe) {
		} finally {
			this.clear(); // 释放资源
		}
	}

	/**
	 * 阻塞挂起执行线程。
	 * <p>
	 * 设置标志位,执行线程的run()方法检查这个标志位,从而阻塞挂起线程,此时可以通过调用start()方法恢复线程的运行。
	 * <p>
	 * 这个方法名称和Thread中public final void suspend()方法一样，但不会有后者的‘固有的死锁倾向’,可以安全的使用
	 */
	public void suspend() {
		this.run = false;
	}

	/**
	 * 终止线程的运行
	 * <p>
	 * 此时不能通过调用start()方法再重新恢复线程的运行。
	 * 
	 */
	public void terminate() {
		try {
			if (this.in != null) {
				this.in.close();
			}
			this.in = null;
			if (this.out != null) {
				this.out.close();
			}
			this.out = null;
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * 终止线程的运行,通过中断线程退出程序。
	 * <p>
	 * 此时不能通过调用start()方法再重新恢复线程的运行。
	 * 
	 */
	public void interrupt() {
		this.executeThread.interrupt();
	}

	/**
	 * 释放资源
	 * 
	 */
	public void clear() {
		if (this.in != null) {
			try {
				this.in.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if (this.out != null) {
			this.out.close();
		}
		this.telent.stopSpyStream();
		this.telent.unregisterNotifHandler();
		this.disconnect();
		logger.info("Closing Telnet Connection Clear Resource...");
	}

	/**
	 * 从缓冲区提取内容,同时把缓冲区清空
	 * 
	 * @return String
	 */
	public synchronized String getResult() {
		String retval = this.output.toString();
		this.output = new StringBuffer();
		try {
			return new String(retval.getBytes(), charSet);
		} catch (UnsupportedEncodingException ue) {
			logger.error("UnsupportedEncodingException - Unsupported Encoding"
					+ ue.getMessage());
			ue.printStackTrace();
		}
		return retval;
	}

	/**
	 * 从缓冲区提取内容
	 * 
	 * @return String
	 */
	public synchronized String getOutput() {
		return this.output.toString();
	}

	/**
	 * 向缓冲区添加内容
	 * 
	 * @param result
	 *            String
	 */
	public synchronized void appendResult(String result) {
		this.output.append(result);
	}

	/**
	 * 作为root用户登录
	 * 
	 * @param password
	 */
	public void suRoot(String password) {
		try {
			write("su");
			read("Password: ");
			write(password);
			this.prompt = "#";
			read(prompt);
			logger.debug("作为root用户登录成功");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 设置基本的配置项
	 * 
	 * @param telnetInfo
	 */
	public void setup(TelnetInfo telnetInfo) {
		// 设置命令标识符,默认$
		this.setPrompt(telnetInfo.getPrompt());
		// 设置字符编码集,默认GBK
		this.setCharSet(telnetInfo.getCharSet());
		// 设置容量因子,默认10
		this.setCapacityFactor(telnetInfo.getCapacityFactor());
	}

	/**
	 * 完成用户的验证并执行bsh命令--为最终执行指令做准备
	 * 
	 * @param telnetInfo
	 */
	public void initCommand(TelnetInfo telnetInfo) {
		String command = telnetInfo.getBaseCommand();
		if (command == null || command.trim().equals(""))
			return;
		this.execute(command);
		String directory = telnetInfo.getDirectory();
		if (directory == null || directory.trim().equals(""))
			return;
		// 设置默认路径（以后可以改变）
		this.execute("cd " + telnetInfo.getDirectory());
	}

	/**
	 * 过滤指令执行的结果
	 * 
	 * @param rs
	 *            原始返回的指令结果
	 * @param cm
	 *            指令
	 * @return
	 */
	public String filterResult(String rs, String cm) {
		if (rs == null || cm == null)
			return rs;
		if (rs.contains(cm))
			rs = StringUtil.deleteFirstLine(rs);
		final String prompt = this.getTelnetInfo().getPrompt();
		if (rs.trim().endsWith(prompt.trim()))
			rs = StringUtil.deleteLastLine(rs);
		return rs.trim();
	}

	/**
	 * Callback method called when TelnetClient receives an option negotiation
	 * command. (non-Javadoc)
	 * 
	 * @see org.apache.commons.net.telnet.TelnetNotificationHandler#receivedNegotiation(int,
	 *      int)
	 * @param negotiation_code
	 * @param option_code
	 */
	public void receivedNegotiation(int negotiation_code, int option_code) {
		String command = null;
		if (negotiation_code == TelnetNotificationHandler.RECEIVED_DO) {
			command = "DO";
		} else if (negotiation_code == TelnetNotificationHandler.RECEIVED_DONT) {
			command = "DONT";
		} else if (negotiation_code == TelnetNotificationHandler.RECEIVED_WILL) {
			command = "WILL";
		} else if (negotiation_code == TelnetNotificationHandler.RECEIVED_WONT) {
			command = "WONT";
		}
		logger.debug("received " + command + " for option code " + option_code);
	}

	/**
	 * public SuppressGAOptionHandler(boolean initlocal, boolean initremote,
	 * boolean acceptlocal, boolean acceptremote) initlocal - - if set to true,
	 * a WILL is sent upon connection. initremote - - if set to true, a DO is
	 * sent upon connection. acceptlocal - - if set to true, any DO request is
	 * accepted. acceptremote - - if set to true, any WILL request is accepted.
	 * 
	 * WILL (option code) 251 指示希望开始执行，或者确认现在正在操作指示的选项。 WON'T (option code)
	 * 252指出拒绝执行或继续招待所指示的选项。 DO (option code) 253 指出要求对方执行，或者确认希望对方执行指示的选项。
	 * DON'T(option code) 254 指出要求对方停止执行，或者确诊要求对方停止执行指示的选项。 IAC 255 数据字节 255。
	 */
	private void addOptionHandler() {
		TerminalTypeOptionHandler ttopt = new TerminalTypeOptionHandler(
				"VT100", false, false, true, false);
		EchoOptionHandler echoopt = new EchoOptionHandler(true, false, true,
				false);
		SuppressGAOptionHandler gaopt = new SuppressGAOptionHandler(true, true,
				true, true);
		try {
			this.telent.addOptionHandler(ttopt);
			this.telent.addOptionHandler(echoopt);
			this.telent.addOptionHandler(gaopt);
			logger.debug("TelnetClient : " + this.telent);
		} catch (InvalidTelnetOptionException e) {
			logger
					.error("Error registering option handlers: "
							+ e.getMessage());
		}
	}

	/**
	 * 释放TELNET连接
	 * 
	 */
	private void disconnect() {
		if (this.telent != null) {
			try {
				this.telent.disconnect();
			} catch (IOException ioe) {
				logger.error("IOException While Closing Telnet -- Notice");
				ioe.printStackTrace();
			}
		}
	}

	/**
	 * 
	 * @param value
	 * 
	 * @return String
	 */
	private String read(String value) {
		if (this.out == null)
			return null;
		char lastChar = value.charAt(value.length() - 1);
		int loopChar = 65535;
		StringBuffer buffer = new StringBuffer();
		char ch;
		try {
			ch = (char) this.in.read();
			while (true) {
				buffer.append(ch);
				if (ch == lastChar) {
					if (buffer.toString().endsWith(value)) {
						successLogin = true;
						return buffer.toString();
					}
				}
				if (ch == loopChar) {// 用户或者密码错误
					logger.debug("ch : " + (int) ch);
					successLogin = false;
					logger.info("因为'用户或者密码'错误,导致无法登录成功");
					this.terminate();
					return null;
				}
				ch = (char) this.in.read();
			}
		} catch (IOException ioe) {
			ioe.printStackTrace();
			logger.error("IOException : " + ioe);
		}
		return null;
	}

	/**
	 * 发送指令
	 * 
	 * @param value
	 */
	private void write(String value) {
		if (this.out == null)
			return;

		try {
			this.out.println(value);
			this.out.flush();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 
	 * @param command
	 * @return
	 */
	private String assembleCommand(String command) {
		if (this.telnetInfo.getEndFlag() == 0)
			return command += ENDFLAG[0];
		if (this.telnetInfo.getEndFlag() == 1)
			return command += ENDFLAG[1];
		return command;
	}

	/**
	 * 
	 * @param cmd
	 */
	private void logCommand(final String cmd) {
		StringBuffer logs = new StringBuffer("user:");
		logs.append(this.telnetInfo.getUserName()).append(" host:").append(
				this.telnetInfo.getHost()).append(" cmd:").append(cmd);
		logger.info(logs.toString());
	}

	public int getCapacityFactor() {
		return capacityFactor;
	}

	public void setCapacityFactor(int capacityFactor) {
		this.capacityFactor = capacityFactor;
	}

	public String getCharSet() {
		return charSet;
	}

	public void setCharSet(String charSet) {
		this.charSet = charSet;
	}

	public String getLoginPrompt() {
		return loginPrompt;
	}

	public void setLoginPrompt(String loginPrompt) {
		this.loginPrompt = loginPrompt;
	}

	public String getPrompt() {
		return prompt;
	}

	public void setPrompt(String prompt) {
		this.prompt = prompt;
	}

	public TelnetInfo getTelnetInfo() {
		return telnetInfo;
	}

	public void setTelnetInfo(TelnetInfo telnetInfo) {
		this.telnetInfo = telnetInfo;
	}

	public boolean isDone() {
		return done;
	}

	public boolean isRun() {
		return run;
	}

	public boolean isSuccessLogin() {
		return successLogin;
	}

	public static void main(String[] args) {
		TelnetInfo telnetInfo = new TelnetInfo("192.168.10.174", "olcom",
				"olcom123");
		TelnetTool telnet = new TelnetTool(telnetInfo);
		final String cm = "ps -ef | grep agent";
		String rs = telnet.execute(cm);
		System.out.println("-----------------------------------");
		System.out.println(rs);
		System.out.println("-----------------------------------");
		String mgs = telnet.filterResult(rs, cm);
		System.out.println(mgs);
		System.out.println("-----------------------------------");
		telnet.terminate();
	}
}